package com.netx.data;
import java.sql.ResultSet;
import java.sql.Statement;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import com.netx.generics.basic.IntegrityException;


public final class Connection {

	private final static byte _COMMIT = 2;
	private final static byte _ROLLBACK = 2;

	private final Database _database;
	private final Map<String,Entity> _entities;
	private java.sql.Connection _connection;
	private List<ConnectionListener> _listeners;
	private boolean _open;
	private Transaction _transaction;
	private Map<String,List<String>> _queries;
	
	Connection(Database d, java.sql.Connection c, Iterator<String> entityNames, List<ConnectionListener> l) throws DatabaseException {
		_database = d;
		_connection = c;
		_listeners = l;
		_open = true;
		_transaction = null;
		_queries = null;
		// init entities:
		_entities = new HashMap<String,Entity>();
		while(entityNames.hasNext()) {
			EntityMetaData metadata = _database.getMetaData(entityNames.next().toString());
			Entity e = metadata.createEntity(this);
			_entities.put(metadata.getName(), e);
		}
		try {
			// just to make sure:
			_connection.setAutoCommit(true);
		}
		catch(SQLException sqle) {
			_treatSQLException(sqle);
			throw _getException(sqle, null, null);
		}
	}

	public void close() throws DatabaseException {
		if(_open) {
			// perform listener actions:
			Iterator<ConnectionListener> it = _listeners.iterator();
			while(it.hasNext()) {
				it.next().connectionClosed(this);
			}
			if(_transaction != null) {
				//TODO _transsaction.rollback();?
				throw new IllegalStateException("entity \""+_transaction.holder.getMetaData().getName()+"\" has an open transaction");
			}
			try {
				_open = false;
				if(_connection.getAutoCommit() == false) {
					throw new IntegrityException();
				}
				_connection.close();
				_connection = null;
			}
			catch(SQLException sqle) {
				_treatSQLException(sqle);
				throw _getException(sqle, null, this);
			}
		}
	}
	
	// for Database:
	void reopen(java.sql.Connection c, List<ConnectionListener> listeners) {
		_connection = c;
		_listeners = listeners;
		_open = true;
	}
	
	public void finalize() throws Exception {
		close();
	}
	
	public Database getDatabase() {
		_checkClosed();
		return _database;
	}
	
	public Entity getEntity(String name) {
		_checkClosed();
		return (Entity)_entities.get(name.toLowerCase());
	}
	
	public com.netx.generics.sql.Table select(String query) throws DatabaseException {
		_checkClosed();
		try {
			Statement stmt = _createStatement();
			Table table = new Table(stmt.executeQuery("SELECT "+query), _database.getDriver());
			stmt.close();
			return table;
		}
		catch(SQLException sqle) {
			if(query.toUpperCase().startsWith("SELECT")) {
				throw new IllegalArgumentException("query cannot begin with SELECT");
			}
			else {
				_treatSQLException(sqle);
				throw _getException(sqle, "SELECT "+query, this);
			}
		}
	}

	// for Entity:
	void startTransaction(Entity e, boolean autoRollback) throws DatabaseException {
		_checkClosed();
		if(_transaction != null) {
			if(_transaction.holder == e) {
				throw new IllegalStateException("a transaction has already been started by this entity");
			}
			else {
				throw new IllegalStateException("a transaction has already been started by entity \""+e.getMetaData().getName()+"\"");
			}
		}
		else {
			try {
				_transaction = new Transaction(e, autoRollback);
				_connection.setAutoCommit(false);
				_queries = new HashMap<String,List<String>>();
			}
			catch(SQLException sqle) {
				_treatSQLException(sqle);
				throw _getException(sqle, null, this);
			}
		}
	}
	
	// for Entity:
	void commit(Entity e) throws DatabaseException {
		_checkClosed();
		_commitOrRollback(e, _COMMIT);
	}
	
	// for Entity:
	void rollback(Entity e) throws DatabaseException {
		_checkClosed();
		_commitOrRollback(e, _ROLLBACK);
	}
	
	// for Entity:
	long performQuery(String query, Entity e, boolean returnKeys, ValueList values) throws DatabaseException {
		try {
			Statement stmt = _createStatement();
			long value = -1;
			if(returnKeys) {
				stmt.execute(query, Statement.RETURN_GENERATED_KEYS);
				ResultSet rs = stmt.getGeneratedKeys();
				rs.next();
				value = rs.getLong(1);
				rs.close();
			}
			else {
				value = stmt.executeUpdate(query);
			}
			stmt.close();
			// save query for transaction:
			if(_transaction != null) {
				List<String> qStrings = _queries.get(e.getMetaData().getName());
				if(qStrings == null) {
					qStrings = new ArrayList<String>();
				}
				qStrings.add(query);
				_queries.put(e.getMetaData().getName(), qStrings);
			}
			return value;
		}
		catch(SQLException sqle) {
			_treatSQLException(sqle);
			throw _getException(sqle, query, this, e, values);
		}
	}
	
	private void _commitOrRollback(Entity e, byte operation) throws DatabaseException {
		if(_transaction == null) {
			throw new IllegalStateException("there is no transaction set");
		}
		else if(_transaction.holder != e) {
			throw new IllegalStateException("transaction is locked by entity \""+_transaction.holder.getMetaData().getName()+"\"");
		}
		else {
			_transaction = null;
			try {
				if(operation == _COMMIT) {
					_connection.commit();
					_updateDataSinks();
				}
				else if(operation == _ROLLBACK) {
					_connection.rollback();
				}
				else {
					throw new IntegrityException(operation);
				}
				_connection.setAutoCommit(true);
				_queries.clear();
				_queries = null;
			}
			catch(SQLException sqle) {
				_treatSQLException(sqle);
				throw _getException(sqle, null, this);
			}
		}
	}

	private void _checkClosed() {
		if(!_open) {
			throw new IllegalStateException("connection is closed");
		}
	}
	
	private Statement _createStatement() throws SQLException {
		return _connection.createStatement(
			ResultSet.TYPE_FORWARD_ONLY,
			ResultSet.CONCUR_READ_ONLY,
			ResultSet.CLOSE_CURSORS_AT_COMMIT
		);
	}
	
	private void _treatSQLException(SQLException sqle) {
		// handle current transaction, if any:
		if(_transaction != null) {
			if(_transaction.autoRollback) {
				_transaction = null;
				_queries.clear();
				_queries = null;
				try {
					_connection.rollback();
				}
				catch(SQLException sqle2) {
					// noop: just rollback if possible
				}
				try {
					_connection.setAutoCommit(true);
				}
				catch(SQLException sqle3) {
					// shouldn't happen:
					throw new IntegrityException(sqle3);
				}
			}
			_transaction = null;
		}
	}
	
	private void _updateDataSinks() {
		Iterator<Map.Entry<String,List<String>>> it = _queries.entrySet().iterator();
		while(it.hasNext()) {
			Map.Entry<String,List<String>> entry = it.next();
			Entity e = getEntity(entry.getKey());
			List<String> qStrings = entry.getValue();
			if(qStrings != null) {
				Iterator<DataSink> backupSinks = getDatabase().getBackupSinks().iterator();
				Iterator<DataSink> replicationSinks = e.getMetaData().getReplicationSinks().iterator();
				_updateSinks(backupSinks, qStrings.iterator());
				_updateSinks(replicationSinks, qStrings.iterator());
			}
		}
	}
	
	private void _updateSinks(Iterator<DataSink> sinks, Iterator<String> queryIt) {
		while(sinks.hasNext()) {
			while(queryIt.hasNext()) {
				sinks.next().update(queryIt.next());
			}
		}
	}

	private DatabaseException _getException(SQLException sqle, String query, Connection c) {
		return _getException(sqle, query, c, null, null);
	}

	private DatabaseException _getException(SQLException sqle, String query, Connection c, Entity e, ValueList values) {
		DatabaseDriver driver = _database.getDriver();
		if(driver.isMalformedQueryException(sqle)) {
			return new MalformedQueryException(c, sqle, query);
		}
		else if(driver.isDatabaseFailureException(sqle)) {
			return new DatabaseFailureException(c, sqle, query);
		}
		else if(driver.isConstraintException(sqle)) {
			ConstraintException ce = new ConstraintException(c, sqle, query);
			Field[] sentFields = new Field[values.size()];
			Object[] sentValues = new Object[values.size()];
			MetaData metadata = e.getMetaData();
			Iterator<Object> itValues = values.iterator();
			for(int i=0; itValues.hasNext(); i++) {
				Value v = (Value)itValues.next();
				sentValues[i] = v.getValue();
				sentFields[i] = metadata.getField(v.getFieldName());
			}
			driver.initConstraintException(ce, sentFields, sentValues);
			return ce;
		}
		else {
			return new UnknownDatabaseException(c, sqle, query);
		}
	}

	private class Transaction {
		public final Entity holder;
		public final boolean autoRollback;
		
		public Transaction(Entity h, boolean ar) {
			holder = h;
			autoRollback = ar;
		}
	}
}
